package com.ccx.demo.business.user.service;

import com.alibaba.fastjson.JSON;
import com.ccx.demo.business.user.cache.ITabRoleCache;
import com.ccx.demo.business.user.dao.jpa.RoleRepository;
import com.ccx.demo.business.user.dto.TabRoleInsertDTO;
import com.ccx.demo.business.user.dto.TabRoleUpdateDTO;
import com.ccx.demo.business.user.entity.QTabRole;
import com.ccx.demo.business.user.entity.TabRole;
import com.ccx.demo.business.user.entity.TabUser;
import com.ccx.demo.business.user.vo.Authority;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.querydsl.core.QueryResults;
import com.support.mvc.entity.base.Page;
import com.support.mvc.exception.DeleteRowsException;
import com.support.mvc.exception.UpdateRowsException;
import com.utils.enums.Code;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.redisson.api.RBatch;
import org.redisson.api.RedissonClient;
import org.redisson.client.codec.StringCodec;
import org.springframework.beans.BeanUtils;
import org.springframework.cache.Cache;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;

import javax.validation.Valid;
import javax.validation.constraints.*;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.ccx.demo.config.init.AppInit.getCacheManager;

/**
 * 服务接口实现类：角色
 *
 * @author 谢长春 on 2019-08-29
 */
@Slf4j
@Service

@Validated
@RequiredArgsConstructor
public class RoleService implements ITabRoleCache {
    /**
     * 管理员角色 ID。 管理员属于特殊角色，禁止前端编辑和删除
     */
    private final RoleRepository repository;
    private final UserService userService;
    private final AuthorityService authorityService;
    private final RedissonClient redissonClient;

    /**
     * 获取当前缓存管理器，用于代码控制缓存
     *
     * @return {@link Cache}
     */
    public Cache getCache() {
        return Objects.requireNonNull(getCacheManager().getCache(CACHE_ROW_BY_ID), "未获取到缓存管理对象:".concat(CACHE_ROW_BY_ID));
    }

    /**
     * 清除多个 key 对应的缓存
     *
     * @param ids {@link TabRole#getId()}
     */
    public void clearKeys(final Collection<String> ids) {
        ids.stream().distinct().forEach(id -> getCache().evict(id));
    }

    /**
     * 保存
     *
     * @param dto    TabRole 实体对象
     * @param userId {@link Long} 操作用户ID
     * @return TabRole 实体对象
     */
    @Transactional(rollbackFor = Exception.class)
    public @NotNull(message = "返回值不能为null") TabRole insert(
            @Valid @NotNull(message = "【dto】不能为null") final TabRoleInsertDTO dto,
            @NotNull(message = "【userId】不能为null") @Positive(message = "【userId】必须大于0") final Long userId) {
        final TabRole obj = new TabRole();
        BeanUtils.copyProperties(dto, obj);
        final String[] authorities = authorityService.expendFilterCheckedSet(obj.getAuthorityTree())
                .stream()
                .map(Authority::getCode)
                .toArray(String[]::new);
        Code.A00003.assertNonEmpty(authorities, "权限指令树【authorityTree】配置无效，有效的权限指令代码不能为空");
        obj.setAuthorities(authorities);
        return repository.insert(userId, obj);
    }

    /**
     * 更新数据
     *
     * @param id     {@link String} 数据ID
     * @param userId {@link Long} 操作用户ID
     * @param dto    TabRole 实体对象
     */
    @Validated
    @Transactional(rollbackFor = Exception.class)
    public void update(
            @NotBlank(message = "【id】不能为空") final String id,
            @NotNull(message = "【userId】不能为null") @Positive(message = "【userId】必须大于0") final Long userId,
            @Valid @NotNull(message = "【dto】不能为null") final TabRoleUpdateDTO dto) {
        final TabRole tabRole = findById(id).orElseThrow(NullPointerException::new);
        Code.A00002.assertHasFalse(
                // 管理员角色权限不能编辑, 只能通过编码的方式授权管理员权限： @PreAuthorize("hasAnyAuthority('ROLE_ADMIN')")
                Objects.equals(tabRole.getHidden(), true),
                "禁止编辑该角色"
        );
        final TabRole obj = new TabRole();
        BeanUtils.copyProperties(dto, obj);
        final String[] authorities = authorityService.expendFilterCheckedSet(obj.getAuthorityTree())
                .stream()
                .map(Authority::getCode)
                .toArray(String[]::new);
        Code.A00003.assertNonEmpty(authorities, "权限指令树【authorityTree】配置无效，有效的权限指令代码不能为空");
        obj.setAuthorities(authorities);
        UpdateRowsException.asserts(repository.update(id, userId, obj));
    }

    /**
     * 按id删除，逻辑删除
     *
     * @param id     {@link String} 数据ID
     * @param userId {@link Long} 操作用户ID
     */
    @Transactional(rollbackFor = Exception.class)
    public void markDeleteById(
            @NotBlank(message = "【id】不能为空") final String id
            , @NotNull(message = "【userId】不能为null") @Positive(message = "【userId】必须大于0") final Long userId) {
        DeleteRowsException.asserts(repository.markDeleteById(id, userId));
    }

    /**
     * 按 id+updateTime 删除，逻辑删除
     *
     * @param id         {@link String} 数据ID
     * @param updateTime {@link String} 最后一次更新时间
     * @param userId     {@link Long} 操作用户ID
     */
    @Transactional(rollbackFor = Exception.class)
    public void markDeleteById(
            @NotBlank(message = "【id】不能为空") final String id
            , @NotBlank(message = "【updateTime】不能为null") @Size(min = 17, max = 17, message = "【updateTime】必须是 17 位") final String updateTime
            , @NotNull(message = "【userId】不能为null") @Positive(message = "【userId】必须大于0") final Long userId
    ) {
        DeleteRowsException.asserts(repository.markDeleteById(id, updateTime, userId));
    }

    /**
     * 批量操作按ID和timestamp删除，逻辑删除
     *
     * @param ids         {@link Set<String>} 数据ID
     * @param updateTimes {@link Set<String>} 数据最后更新时间
     * @param userId      {@link Long} 操作用户ID
     */
    @Validated
    @Transactional(rollbackFor = Exception.class)
    public void markDelete(
            @NotEmpty(message = "【ids】不能为空") final Set<@Valid @NotNull String> ids,
            final Set<String> updateTimes,
            @NotNull(message = "【userId】不能为null") @Positive(message = "【userId】必须大于0") final Long userId) {
        if (repository.exists(QTabRole.tabRole.id.in(ids)
                .and(QTabRole.tabRole.hidden.eq(true))
        )) {
            throw new RuntimeException("禁止删除该角色");
        }
        DeleteRowsException.asserts(repository.markDelete(ids, updateTimes, userId), ids.size());
        clearKeys(ids);
    }

    /**
     * 角色表 按ID查询对象，注意这里可能有 deleted 为 YES 的数据
     *
     * @param id {@link String} 数据ID
     * @return {@link Optional<TabRole>} 实体对象
     */
    @Transactional(readOnly = true, propagation = Propagation.NOT_SUPPORTED)
    public Optional<TabRole> findById(final String id) {
        if (Strings.isNullOrEmpty(id)) {
            return Optional.empty();
        }
//        return repository.findById(id)
//                .map(TabRole::cloneObject); // 必须要 clone ，如果直接对持久化对象调用 set 方法，会触发更新动作
        return Optional.ofNullable(repository.findCacheById(id)).map(TabRole::cloneObject); // 若使用缓存需要解开代码
    }

    /**
     * 按条件分页查询列表
     *
     * @param condition TabRole 查询条件
     * @param page      {@link Page} 分页排序集合
     * @return {@link QueryResults<TabRole>} 分页对象
     */
    @Transactional(readOnly = true, propagation = Propagation.NOT_SUPPORTED)
    public @NotNull(message = "返回值不能为null") QueryResults<TabRole> page(
            @NotNull(message = "【condition】不能为null") final TabRole condition,
            @NotNull(message = "【page】不能为null") @Valid final Page page) {
        condition.setHidden(false); // 禁止前端选择权限时，看到管理员（ROLE_ADMIN）角色。 该角色数据静态角色，用户只能操作配置出来的角色
        final QueryResults<TabRole> queryResults = repository.page(condition, page);
        if (queryResults.isEmpty()) {
            return QueryResults.emptyResults();
        }
        final Map<Long, TabUser> userMap = userService.mapByIds(queryResults.getResults().stream()
                .flatMap(row -> Stream.of(row.getCreateUserId(), row.getUpdateUserId()))
                .filter(Objects::nonNull)
                .collect(Collectors.toSet())
        );
        queryResults.getResults().forEach(row -> {
            row.setAuthorityTree(
                    authorityService.getTree(Sets.newHashSet(row.getAuthorities()))
            );
            Optional.ofNullable(userMap.get(row.getCreateUserId())).ifPresent(user -> {
                row.setCreateUserNickname(user.getNickname());
            });
            Optional.ofNullable(userMap.get(row.getUpdateUserId())).ifPresent(user -> {
                row.setUpdateUserNickname(user.getNickname());
            });
        });
        return queryResults;
    }

    /**
     * 匹配有效的角色id
     *
     * @param roleIds {@link Set<String>} 角色集合
     * @return {@link Set<String>}
     */
    public Set<String> matchValidRoleIds(final Set<String> roleIds) {
        if (CollectionUtils.isEmpty(roleIds)) {
            return Collections.emptySet();
        }
//        return repository.findValidRoleIds(roles);
        return roleIds.stream()
                // 从缓存获取数据
                .map(id -> getTabRoleCacheById(id).orElse(null))
                // 排除已删除的角色
                .filter(role -> Objects.nonNull(role) && Objects.equals(false, role.getDeleted()))
                .map(TabRole::getId)
                .collect(Collectors.toSet())
                ;
    }

    /**
     * 获取前端角色选项集合
     *
     * @return {@link List<TabRole>}
     */
    @Transactional(readOnly = true, propagation = Propagation.NOT_SUPPORTED)
    public List<TabRole> getOptions() {
        final QTabRole table = QTabRole.tabRole;
        final TabRole condition = new TabRole();
        condition.setHidden(false); // 禁止前端选择权限时，看到管理员（ROLE_ADMIN）角色。 该角色数据静态角色，用户只能操作配置出来的角色
        return repository.list(
                condition,
                table.id,
                table.name,
                table.updateTime
        );
    }

    /**
     * 角色表 按 id 批量查询列表，注意这里可能有 deleted 为 true 的数据
     *
     * @param ids {@link String}  数据 id 集合
     * @return {@link List<TabRole>} 结果集合
     */
    @Transactional(readOnly = true, propagation = Propagation.NOT_SUPPORTED)
    public @NotNull(message = "返回值不能为null") List<TabRole> listByIds(final Collection<String> ids) {
        if (org.springframework.util.CollectionUtils.isEmpty(ids)) {
            return Collections.emptyList();
        }
        return Lists.newArrayList(mapByIds(Sets.newHashSet(ids)).values());
    }

    /**
     * 角色表 按 id 批量查询列表，返回 map ， key 为数据 id ， 注意这里可能有 deleted 为 true 的数据
     *
     * @param ids {@link String} 数据 id 集合
     * @return {@link List<TabRole>} 结果集合
     */
    @Transactional(readOnly = true, propagation = Propagation.NOT_SUPPORTED)
    public @NotNull(message = "返回值不能为null") Map<String, TabRole> mapByIds(final Set<String> ids) {
        if (org.springframework.util.CollectionUtils.isEmpty(ids)) {
            return Collections.emptyMap();
        }
        final RBatch batch = redissonClient.createBatch();
        ids.stream().distinct().forEach(id -> batch.getBucket(cacheKeyById(id), StringCodec.INSTANCE).getAsync());
        // 批量从缓存拿数据
        final Map<String, TabRole> cacheMap = batch.execute()
                .getResponses()
                .stream()
                // 过滤 null 值， null 表示缓存中不存在，需要从数据库查
                .filter(Objects::nonNull)
                .map(jsonText -> JSON.parseObject((String) jsonText, TabRole.class))
                .collect(Collectors.toMap(TabRole::getId, Function.identity()));
        // 提取缓存中存在的数据ID
        final Set<String> cacheExistIds = cacheMap.keySet();
        ids.stream().distinct()
                // 排除缓存中存在的数据 id
                .filter(id -> !cacheExistIds.contains(id))
                .forEach(id -> {
                    // 从数据库查询，并放入缓存
                    final TabRole cacheObject = repository.findCacheById(id);
                    if (Objects.nonNull(cacheObject)) {
                        cacheMap.put(id, cacheObject);
                    }
                });
        return cacheMap;
    }
}
